import { spawn } from 'child_process';
import { DecompileResult } from './types.js';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as crypto from 'crypto';

interface DecompileCacheEntry {
  result: DecompileResult;
  timestamp: number;
  hash: string;
}

export class JavaDecompiler {
  private cfr_path: string;
  private fernflower_path: string;
  private decompileCache = new Map<string, DecompileCacheEntry>();
  private readonly CACHE_TTL = 3600000; // 1 hour
  private readonly MAX_CACHE_ENTRIES = 500;

  constructor() {
    this.cfr_path = process.env.CFR_PATH || path.join(process.cwd(), 'tools', 'cfr.jar');
    this.fernflower_path = process.env.FERNFLOWER_PATH || 'fernflower';
  }

  async decompileClass(classBytes: Buffer | string, className: string): Promise<DecompileResult> {
    console.error(`[JavaDecompiler] 开始反编译类: ${className}`);
    
    // Generate cache key
    const cacheKey = this.getCacheKey(classBytes, className);
    
    // Check cache
    const cached = this.decompileCache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
      console.error(`[JavaDecompiler] 使用缓存的反编译结果`);
      return cached.result;
    }
    
    let result: DecompileResult;
    
    if (typeof classBytes === 'string') {
      console.error(`[JavaDecompiler] 从JAR文件反编译: ${classBytes}`);
      result = await this.decompileFromJar(classBytes, className);
    } else {
      console.error(`[JavaDecompiler] 从内存字节码反编译，大小: ${classBytes.length} 字节`);
      const tempDir = `/tmp/decompile-${Date.now()}`;
      const tempClassFile = path.join(tempDir, `${className.replace(/\./g, '/')}.class`);
      
      try {
        console.error(`[JavaDecompiler] 创建临时目录: ${tempDir}`);
        await fs.mkdir(path.dirname(tempClassFile), { recursive: true });
        
        console.error(`[JavaDecompiler] 写入临时class文件: ${tempClassFile}`);
        await fs.writeFile(tempClassFile, classBytes);
        
        // Try CFR first
        console.error(`[JavaDecompiler] 尝试使用CFR反编译...`);
        result = await this.decompileWithCFR(tempClassFile);
        
        console.error(`[JavaDecompiler] 清理临时文件...`);
        await this.cleanupTemp(tempDir);
        
        // If CFR failed, try simple decompiler
        if (!result.success) {
          console.error(`[JavaDecompiler] CFR反编译失败，使用基础反编译器`);
          result = await this.decompileWithSimpleDecompiler(classBytes as Buffer);
        } else {
          console.error(`[JavaDecompiler] CFR反编译成功`);
        }
      } catch (error) {
        console.error(`[JavaDecompiler] CFR反编译出错: ${error}，使用基础反编译器`);
        await this.cleanupTemp(tempDir);
        result = await this.decompileWithSimpleDecompiler(classBytes as Buffer);
      }
    }
    
    // Cache the result
    this.addToCache(cacheKey, result, classBytes);
    
    return result;
  }

  async decompileFromJar(jarPath: string, className: string): Promise<DecompileResult> {
    try {
      const result = await this.decompileWithCFR(jarPath, className);
      return result;
    } catch (error) {
      return {
        success: false,
        error: `Decompilation failed: ${error}`
      };
    }
  }

  private async decompileWithCFR(input: string, className?: string): Promise<DecompileResult> {
    console.error(`[JavaDecompiler] CFR配置 - CFR路径: ${this.cfr_path}`);
    console.error(`[JavaDecompiler] CFR输入: ${input}${className ? `, 目标类: ${className}` : ''}`);
    
    return new Promise(async (resolve) => {
      try {
        // Create a temporary output directory
        const tempOutputDir = `/tmp/cfr-output-${Date.now()}`;
        console.error(`[JavaDecompiler] 创建CFR输出目录: ${tempOutputDir}`);
        await fs.mkdir(tempOutputDir, { recursive: true });
        
        const args = ['--outputdir', tempOutputDir, '--silent', 'false', '--comments', 'true', '--recover', 'true'];
        
        if (className) {
          args.push('--specificclass', className);
        }
        
        args.push(input);
        
        console.error(`[JavaDecompiler] 执行CFR命令: java -jar ${this.cfr_path} ${args.join(' ')}`);
        const process = spawn('java', ['-jar', this.cfr_path, ...args]);
        
        let stdout = '';
        let stderr = '';
        
        process.stdout.on('data', (data) => {
          const output = data.toString();
          stdout += output;
          if (output.trim()) {
            console.error(`[JavaDecompiler] CFR stdout: ${output.trim()}`);
          }
        });
        
        process.stderr.on('data', (data) => {
          const output = data.toString();
          stderr += output;
          if (output.trim()) {
            console.error(`[JavaDecompiler] CFR stderr: ${output.trim()}`);
          }
        });
        
        process.on('close', async (code) => {
          try {
            if (code === 0 || code === null) {
              // Find the generated Java file
              let outputFile;
              if (className) {
                // For specific class, construct the path
                const packagePath = className.substring(0, className.lastIndexOf('.')).replace(/\./g, '/');
                const simpleClassName = className.substring(className.lastIndexOf('.') + 1);
                outputFile = path.join(tempOutputDir, packagePath, `${simpleClassName}.java`);
              } else {
                // For class file input, derive the path
                const baseName = path.basename(input, '.class');
                outputFile = path.join(tempOutputDir, `${baseName}.java`);
              }
              
              try {
                const source = await fs.readFile(outputFile, 'utf-8');
                // Cleanup
                await fs.rm(tempOutputDir, { recursive: true, force: true });
                
                resolve({
                  success: true,
                  source
                });
              } catch (readError) {
                // Try to find any Java files in the output directory
                try {
                  const findJavaFiles = async (dir: string): Promise<string[]> => {
                    const files: string[] = [];
                    const items = await fs.readdir(dir, { withFileTypes: true });
                    
                    for (const item of items) {
                      const fullPath = path.join(dir, item.name);
                      if (item.isDirectory()) {
                        files.push(...await findJavaFiles(fullPath));
                      } else if (item.name.endsWith('.java')) {
                        files.push(fullPath);
                      }
                    }
                    return files;
                  };
                  
                  const javaFiles = await findJavaFiles(tempOutputDir);
                  if (javaFiles.length > 0) {
                    const source = await fs.readFile(javaFiles[0], 'utf-8');
                    await fs.rm(tempOutputDir, { recursive: true, force: true });
                    
                    resolve({
                      success: true,
                      source
                    });
                  } else {
                    await fs.rm(tempOutputDir, { recursive: true, force: true });
                    resolve({
                      success: false,
                      error: `No Java files found in output directory. CFR stderr: ${stderr}`
                    });
                  }
                } catch (findError) {
                  await fs.rm(tempOutputDir, { recursive: true, force: true });
                  resolve({
                    success: false,
                    error: `Failed to find generated Java file: ${findError}. CFR stderr: ${stderr}`
                  });
                }
              }
            } else {
              await fs.rm(tempOutputDir, { recursive: true, force: true });
              resolve({
                success: false,
                error: stderr || `CFR process exited with code ${code}`
              });
            }
          } catch (error) {
            resolve({
              success: false,
              error: `Error processing CFR output: ${error}`
            });
          }
        });
        
        process.on('error', async (error) => {
          try {
            await fs.rm(tempOutputDir, { recursive: true, force: true });
          } catch {}
          resolve({
            success: false,
            error: `Failed to start CFR process: ${error}`
          });
        });
      } catch (error) {
        resolve({
          success: false,
          error: `Failed to setup CFR decompilation: ${error}`
        });
      }
    });
  }

  private async decompileWithSimpleDecompiler(classBytes: Buffer): Promise<DecompileResult> {
    try {
      const source = this.basicClassDecompile(classBytes);
      return {
        success: true,
        source
      };
    } catch (error) {
      return {
        success: false,
        error: `Basic decompilation failed: ${error}`
      };
    }
  }

  private getCacheKey(classBytes: Buffer | string, className: string): string {
    if (typeof classBytes === 'string') {
      // For JAR path, use path and class name
      return `jar:${classBytes}:${className}`;
    } else {
      // For byte arrays, use hash
      const hash = crypto.createHash('md5').update(classBytes).digest('hex');
      return `bytes:${hash}:${className}`;
    }
  }

  private addToCache(cacheKey: string, result: DecompileResult, classBytes: Buffer | string): void {
    // Evict old entries if cache is too large
    if (this.decompileCache.size >= this.MAX_CACHE_ENTRIES) {
      const oldestKey = this.decompileCache.keys().next().value;
      if (oldestKey) {
        this.decompileCache.delete(oldestKey);
        console.error(`[JavaDecompiler] 缓存已满，移除最旧的条目`);
      }
    }
    
    const hash = typeof classBytes === 'string' ? 
      classBytes : 
      crypto.createHash('md5').update(classBytes).digest('hex');
    
    this.decompileCache.set(cacheKey, {
      result,
      timestamp: Date.now(),
      hash
    });
    
    console.error(`[JavaDecompiler] 已缓存反编译结果，缓存大小: ${this.decompileCache.size} 个条目`);
  }

  clearCache(): void {
    this.decompileCache.clear();
    console.error(`[JavaDecompiler] 反编译缓存已清空`);
  }

  private basicClassDecompile(classBytes: Buffer): string {
    const magic = classBytes.readUInt32BE(0);
    if (magic !== 0xCAFEBABE) {
      throw new Error('Invalid class file magic');
    }
    
    const minorVersion = classBytes.readUInt16BE(4);
    const majorVersion = classBytes.readUInt16BE(6);
    
    return `// Decompiled Java class
// Java version: ${this.getJavaVersion(majorVersion)}
// Minor version: ${minorVersion}
// Major version: ${majorVersion}

// Note: This is a basic decompiler. For full decompilation, install CFR or Fernflower.
public class DecompiledClass {
    // Class bytecode detected but full decompilation requires external tools
    // Install CFR: Download from https://github.com/leibnitz27/cfr
    // Set CFR_PATH environment variable to the CFR JAR path
}`;
  }

  private getJavaVersion(majorVersion: number): string {
    const versions: { [key: number]: string } = {
      45: '1.1',
      46: '1.2',
      47: '1.3',
      48: '1.4',
      49: '5',
      50: '6',
      51: '7',
      52: '8',
      53: '9',
      54: '10',
      55: '11',
      56: '12',
      57: '13',
      58: '14',
      59: '15',
      60: '16',
      61: '17',
      62: '18',
      63: '19',
      64: '20',
      65: '21'
    };
    
    return versions[majorVersion] || `Unknown (${majorVersion})`;
  }

  private async cleanupTemp(dir: string): Promise<void> {
    try {
      await fs.rm(dir, { recursive: true, force: true });
    } catch (error) {
      console.error(`Failed to cleanup temp directory: ${error}`);
    }
  }
}